package shake.event;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.log4j.Logger;

import shake.annotation.Observer;
import shake.init.module.ComponentDescriptor;

import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.Singleton;
import com.google.inject.name.Names;

@Singleton
public class EventInvoker {
	@Inject
	Logger logger;

	Map<Key<?>, List<KeyAndMethod>> observers = new ConcurrentHashMap<Key<?>, List<KeyAndMethod>>();

	public List<KeyAndMethod> getObservers(Key<?> type) {
		List<KeyAndMethod> list = observers.get(type);
		if (list != null) {
			return list;
		}
		list = new CopyOnWriteArrayList<KeyAndMethod>();
		observers.put(type, list);
		return list;
	}

	public void install(ComponentDescriptor componentDescriptor) {
		ArrayList<Method> installedMethods = new ArrayList<Method>();

		for (Method m : componentDescriptor.getObserverMethod()) {
			Observer observer = m.getAnnotation(Observer.class);
			String name = observer.name();
			Key<?> type;
			if (name.length() == 0) {
				type = Key.get(observer.value());
			} else {
				type = Key.get(observer.value(), Names.named(name));
			}
			getObservers(type).add(
					new KeyAndMethod(componentDescriptor.getKey(), m));
			installedMethods.add(m);
		}
	}

	public void invoke(com.google.inject.Injector injector,
			ParameterizedType type, String name, Object... param) {
		Key<?> key;
		if (name == null || name.length() == 0) {
			key = Key.get(type.getActualTypeArguments()[0]);
		} else {
			key = Key.get(type.getActualTypeArguments()[0], Names.named(name));
		}
		if (logger.isDebugEnabled()) {
			if (name != null)
				logger.debug("fire event " + type + "\t" + name + "\t" + param);
			if (name == null)
				logger.debug("fire event " + type + "\t" + param);
		}

		List<KeyAndMethod> observers = getObservers(key);
		OBSERVERS: for (KeyAndMethod oberber : observers) {
			Method m = oberber.getMethod();
			Type[] types = m.getGenericParameterTypes();
			Object[] p = new Object[types.length];
			for (int i = 0; i < p.length; i++) {
				if (i < param.length) {
					if (param[i] != null && !isCast(types[i], param[i])) {
						continue OBSERVERS;
					}
					p[i] = param[i];
				}
			}
			try {
				Object obj = injector.getInstance(oberber.getKey());
				m.invoke(obj, p);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	private boolean isCast(Type type, Object object) {
		if (object == null) {
			return true;
		}
		if (type instanceof ParameterizedType) {
			ParameterizedType t = (ParameterizedType) type;
			if (((Class) t.getRawType()).isAssignableFrom(object.getClass())) {
				return true;
			} else {
				return false;
			}
		}
		if (type instanceof Class) {
			if (((Class) type).isAssignableFrom(object.getClass())) {
				return true;
			} else {
				return false;
			}
		}

		return false;
	}
}
